<?php
namespace Tbf\Bundle\DatabaseBundle\SetTableData;
use Doctrine\DBAL\Connection;
use Tbf\Bundle\DatabaseBundle\Exception\DatabaseException;

/**
 * 设置数据库某些张表的数据
 * 重点优化:时间性能
 * 目前检测下列输入错误
 * 1.表不存在
 * 2.字段不存在
 * 3.输入格式错误
 * */
class SetTableData
{
    /** @var  Connection */
    protected $connect;
    /** @var  MergeSql */
    protected $query;
    protected $db_array = array();

    public function __construct(Connection $connect)
    {
        $this->connect = $connect;
    }

    protected function getDbConnection()
    {
        return $this->connect;
    }

    /**
     * 根据测试的mock数据重置数据库
     * @param $dbArray array map[string][]map[string]string
     * @return void
     * 输入格式:
     *   array(
     *    'cart_man'=>array(
     *       array(
     *         'manid'=>1,
     *         'ispublish'=>'1',
     *       )
     *     )
     *  )
     * 可以缺少某些字段:表示不关心那些字段的值,
     *
     * 1.使用innodb,事务,truncate,单条insert多数据插入
     * 2.使用innodb,没有开事务,... 没有性能提升
     * 3.使用innodb,使用READ UNCOMMITTED隔离等级,没有性能提升
     * 4.使用myisam,... 明显性能提升(这个地方不是最费时间的了)
     * 5.使用memory...  明显性能提升(这个地方不是最费时间的了)
     * 6.使用innodb,将truncate改为delete from + alter table,没有性能变化
     * 7.使用SET UNIQUE_CHECKS = 0,没有较大性能变化...
     * 8.判断数据是否需要修改, 提升整体性能约一倍...
     * 9.判断autoIncrement的值,再确定是否truncate table,性能有小幅提升
     * 10.使用unit of work 提升此处性能约一倍...
     * 11.将所有sql塞入一个query调用中执行,减少tcp连接损失时间 性能有微小提升...
     * 结论:
     * 1.是否开事务,事务的隔离等级,对此处性能无较大影响
     * 2.使用memory引擎,可以较大程度提高性能
     * TODO: 根据数据库已有信息将操作分为delete update insert 分别进行大批量操作
     * TODO: 使用show table status里面的rows的值,快速过滤表是否有变化(当变化时,不查询select *)(提升水平太小,仅占0.13%)
     */
    public function setTableData($dbArray)
    {
        $this->query = new MergeSql($this->connect);
        foreach ($dbArray as $table_name => $expect_data) {
            $this->setOneTableData($table_name, $expect_data);
        }
        if ($this->query->isEmptyQuery()) {
            return;
        }
        $this->query->addQueryBefore(MergeSql::BEFORE_TRANSACTION,'SET FOREIGN_KEY_CHECKS=0;SET UNIQUE_CHECKS = 0;');
        $this->query->addQuery(MergeSql::AFTER_TRANSACTION,'SET FOREIGN_KEY_CHECKS=1;SET UNIQUE_CHECKS = 1;');
        $this->query->runAllQuery();
    }

    /**
     * 设置一张表的数据
     * 目标: 将这张表上的数据设置为需要设置的数据,自增键的值必须是最大id+1 时间性能尽可能高
     * 环境: 很多时候,其实这张表上的数据就是需要数据,没有任何变化,或者有的几行变化
     * 实现:
     * 1.确定自增键的值是否比需要的大 show table status 获取主键的名称
     *    如果比需要的大
     *        1.->truncate table -> 重新插入所有数据
     *        2.-> 根据原有数据分别进行 delete update insert ->alter table
     * 2.确定现有数据是否和需要的数据完全一致 select * from
     *    如果一致,什么也不做
     * 3.根据原有数据分别进行 delete update insert
     * @param $table_name 表名
     * @param $expect_data 需要设置上去的数据
     */
    protected function setOneTableData($table_name, $expect_data)
    {
        $this->checkColumnFieldExists($table_name,$expect_data);
        //全部删掉
        if (!$this->isNeedResetAi($table_name, $expect_data)) {
            //如果数据库表里面的已有数据和需要插入的数据一致,则什么也不做
            $table_data = $this->getDbConnection()->query('select * from `' . $table_name . '`')->fetchAll(\PDO::FETCH_ASSOC);
            if ($this->checkSameTableData($table_data, $expect_data)) {
                return;
            }
            $this->query->addQuery(MergeSql::TRANSACTION,'delete from `' . $table_name . '`');
            //批量插入
            $this->bulkInsert($table_name, $expect_data);
        } else {
            $this->query->addQuery(MergeSql::BEFORE_TRANSACTION,'truncate `' . $table_name . '`');
            //批量插入
            $this->bulkInsert($table_name, $expect_data);
        }
    }

    protected function isNeedResetAi($table_name, $expect_data)
    {
        $table_status = $this->getDbConnection()->query('show table status where Name="' . $table_name . '"')->fetchAll(\PDO::FETCH_ASSOC);
        if (empty($table_status)) {
            throw new DatabaseException('set table data: table:'.$table_name.' not exist');
        }
        $current_ai = $table_status[0]['Auto_increment'];
        if ($current_ai === null) {
            //没有自增值,不需要修改自增值
            return false;
        }
        if ($current_ai == 1) {
            //自增值是1,说明表是空的,不需要修改自增值
            return false;
        }
        if (($current_ai - 1) <= count($expect_data)) {
            //自增值比所有数据总数都小..
            return false;
        }
        if (count($expect_data) === 0) {
            //自增值不是1,但是需要清空数据库,一定需要修改自增值
            return true;
        }
        //有自增主键
        $pk_name = $this->getPrimaryKeyColumnsOnTable($table_name);
        $max_id_value = 0;
        $no_pk_row_num = 0;
        foreach ($expect_data as $row) {
            if (empty($row[$pk_name])) {
                $no_pk_row_num += 1;
                continue;
            }
            if ($row[$pk_name] > $max_id_value) {
                $max_id_value = $row[$pk_name];
            }
        }
        $max_id_value += $no_pk_row_num;
        if (($current_ai - 1) > $max_id_value) {
            return true;
        }

        return false;
    }

    /**
     * @param $table_name
     * @return \Doctrine\DBAL\Schema\Table
     */
    protected function getTableDetails($table_name){
        static $cache_table = array();
        if (empty($cache_table[$table_name])) {
            $cache_table[$table_name] = $this->getDbConnection()->getSchemaManager()->listTableDetails($table_name);
        }
        return $cache_table[$table_name];
    }

    /**
     * 检查输入字段名是否正确, 由于pdo的bug 将所有sql塞入一个query调用中执行,如中间某一个出现错误,整个执行不会报错!!!
     */
    protected function checkColumnFieldExists($table_name,$expect_data){
        return ;
        if (empty($expect_data)){
            return;
        }
        $column_map = array();
        foreach($this->getTableDetails($table_name)->getColumns() as $column){
            $column_map[$column->getName()] = 1;
        }

        foreach($expect_data as $row){
            foreach($row as $k1=>$v1){
                if (empty($column_map[$k1])){
                    throw new DatabaseException(sprintf('column %s on table %s not exists',$k1,$table_name));
                }
            }
        }
    }
    protected function getPrimaryKeyColumnsOnTable($table_name)
    {
        $data = $this->getTableDetails($table_name)->getPrimaryKeyColumns();
        return $data[0];
    }

    protected function checkSameTableData($table_data, $expect_data)
    {
        if (count($table_data) !== count($expect_data)) {
            return false;
        }
        foreach ($expect_data as $k1 => $row) {
            if (empty($table_data[$k1])) {
                return false;
            }
            foreach ($row as $k2 => $value) {
                if (!array_key_exists($k2, $table_data[$k1]) || $table_data[$k1][$k2] != $value) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @param $table_data
     * @param $expect_data
     * TODO: 完成
     */
    protected function fastSetData($table_data, $expect_data)
    {
        //批量删除多余的
        //批量更新变化的
        //批量插入新加的
    }

    /**
     * 同一张表上 大批量快速插入
     * 1.输入数据必须是[]map[string]string 格式
     */
    protected function bulkInsert($table_name, $data)
    {
        $i = 0;
        $cols = array();
        foreach ($data as $row) {
            if (!is_array($row)){
                throw new DatabaseException('bulkInsert wrong data format!!type:[]map[string]string');
            }
            foreach ($row as $columnName => $value) {
                $cols[$i]['column'][$columnName] = 1;
                $cols[$i]['placeholder_array'][] = '?';
                $cols[$i]['value'][] = $value;
            }
            $cols[$i]['placeholder'] = '(' . implode(', ', $cols[$i]['placeholder_array']) . ')';
            $i++;
        }
        //没有数据就退出了
        if (empty($cols)) {
            return;
        }
        //每一行的列不一定是一样的...
        while (!empty($cols)) {
            $this_cols = array(array_shift($cols)); //尽量保持插入顺序
            foreach ($cols as $i => $col) {
                if ($col['column'] === $this_cols[0]['column']) {
                    $this_cols[] = $col;
                    unset($cols[$i]);
                }
            }
            $placeholders = array();
            $values = array();
            foreach ($this_cols as $i => $col) {
                $placeholders[] = $col['placeholder'];
                $values = array_merge($values, $col['value']);
            }
            $col_name_list = array_keys($this_cols[0]['column']);
            $sql = 'INSERT INTO ' . $table_name
                . ' (`' . implode('`, `', $col_name_list) . '`)'
                . ' VALUES ' . implode(', ', $placeholders);
            $this->query->addQuery(MergeSql::TRANSACTION,$sql,$values);
        }
    }
}
